package com.longtxt.core.ble.main

import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.BOND_BONDED
import android.bluetooth.BluetoothDevice.BOND_BONDING
import android.bluetooth.BluetoothDevice.BOND_NONE
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothStatusCodes
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import com.band.smartband.core.ble.GattStatus
import com.longtxt.core.ble.util.bytesToHex
import timber.log.Timber
import java.util.Arrays
import java.util.Collections
import java.util.Locale
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue


@SuppressLint("MissingPermission")
@Suppress("unused", "deprecation")
class BlePeripheral(
    private val context: Context,
    private val bleDevice: BluetoothDevice,
    private val blePeripheralCallback: BlePeripheralCallback,
    private val timeoutCallback: (Void) -> Unit
) {

    private var bluetoothGatt: BluetoothGatt? = null

    private val commandQueue = ConcurrentLinkedQueue<Runnable>()

    @Volatile
    private var commandQueueBusy = false
    private var retries = 0
    private var cachedName = ""
    private var connectTimestamp: Long = 0
    private var currentCommand = IDLE

    //    private var currentWriteValue = ""
    private var discoveryStarted = false
    private var isRetrying = false
    private val bleHandler = Handler(Looper.getMainLooper())
    private var timeoutRunnable: Runnable? = null
    private var discoverServicesRunnable: Runnable? = null
    private val notifyingCharacteristics: MutableSet<BluetoothGattCharacteristic> = HashSet()

    private var state = BluetoothProfile.STATE_DISCONNECTED

    private var txChar: BluetoothGattCharacteristic? = null
    private var rxChar: BluetoothGattCharacteristic? = null
    private var txJLChar: BluetoothGattCharacteristic? = null
    private var rxJLChar: BluetoothGattCharacteristic? = null


    val isBond: Boolean
        get() {
            return bleDevice.bondState == BOND_BONDED || bleDevice.type != BluetoothDevice.DEVICE_TYPE_DUAL
        }

    /**
     * Returns the currently set MTU
     *
     * @return the MTU
     */
    var currentMtu = DEFAULT_MTU
        private set

    val mDevice: BluetoothDevice
        get() = bleDevice

    val mGatt: BluetoothGatt?
        get() = bluetoothGatt

    internal var queuedCommands: Int = 0
        get() = commandQueue.size
        private set

    /**
     * This abstract class is used to implement BluetoothGatt callbacks.
     */
    private val bluetoothGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            Logger.e(TAG, "connection state $state, newState: $newState")
            blePeripheralCallback.onConnectStatus(gatt.device, newState)
            if (newState != BluetoothProfile.STATE_CONNECTING) cancelConnectionTimer()
            val previousState = state
            state = newState

            val hciStatus = HciStatus.Companion.fromValue(status)
            if (hciStatus == HciStatus.SUCCESS) {
                when (newState) {
                    BluetoothProfile.STATE_CONNECTED -> successfullyConnected()
                    BluetoothProfile.STATE_DISCONNECTED -> successfullyDisconnected(previousState)
                    BluetoothProfile.STATE_DISCONNECTING -> {
                        Logger.i(TAG, "peripheral '%s' is disconnecting", address)
                        blePeripheralCallback.disconnecting(this@BlePeripheral)
                    }

                    BluetoothProfile.STATE_CONNECTING -> {
                        Logger.i(TAG, "peripheral '%s' is connecting", address)
                        blePeripheralCallback.connecting(this@BlePeripheral)
                    }

                    else -> Logger.e(TAG, "unknown state received")
                }
            } else {
                connectionStateChangeUnsuccessful(hciStatus, previousState, newState)
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(
                    TAG,
                    "service discovery failed due to internal error '%s', disconnecting",
                    gattStatus
                )
                disconnect()
                return
            }
            Logger.i(TAG, "discovered %d services for '%s'", gatt.services.size, name)
            val mainService = gatt.getService(MAIN_SERVICE_UUID)
            if (mainService == null) {
                Logger.e(TAG, "service discovery failed, disconnecting")
                disconnect()
                return
            }
            if (mainService.getCharacteristic(RX_CHAR_UUID) == null) {
                Logger.e(TAG, "service discovery failed, disconnecting")
                disconnect()
                return
            }
            if (mainService.getCharacteristic(TX_CHAR_UUID) == null) {
                Logger.e(TAG, "service discovery failed, disconnecting")
                disconnect()
                return
            }
            txChar = mainService.getCharacteristic(TX_CHAR_UUID)
            rxChar = mainService.getCharacteristic(RX_CHAR_UUID)
            try {
                setNotify(rxChar!!, true)
            } catch (e: Exception) {
                Logger.e(TAG, "setNotify failed")
            }
            val mainJlService = gatt.services.find { it.uuid == JL_SERVICE_UUID }
            if (mainJlService != null) {
                txJLChar = mainJlService.getCharacteristic(JL_TX_UUID)
                rxJLChar = mainJlService.getCharacteristic(JL_RX_UUID)

                try {
                    setNotify(rxJLChar!!, true)
                } catch (e: Exception) {
                    Logger.e(TAG, "setNotify failed")
                }
                blePeripheralCallback.onJlWatchFound(true)
            } else {
                blePeripheralCallback.onJlWatchFound(false)
            }
            bleHandler.postDelayed({ requestMtu(MAX_MTU) }, DIRECT_CONNECTION_DELAY_IN_MS)
        }

        override fun onDescriptorWrite(
            gatt: BluetoothGatt,
            descriptor: BluetoothGattDescriptor,
            status: Int
        ) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            val parentCharacteristic = descriptor.characteristic
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(
                    TAG,
                    "failed to write <%s> to descriptor of characteristic <%s> for device: '%s', status '%s' ",
                    "",
                    parentCharacteristic.uuid,
                    address,
                    gattStatus
                )
            }
            gatt.readDescriptor(descriptor)
        }

        // NOTE the signature of this method is inconsistent with the other callbacks, i.e. position of status
        override fun onDescriptorRead(
            gatt: BluetoothGatt,
            descriptor: BluetoothGattDescriptor,
            status: Int,
            value: ByteArray
        ) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(
                    TAG,
                    "reading descriptor <%s> failed for device '%s, status '%s'",
                    descriptor.uuid,
                    address,
                    gattStatus
                )
            }
            // Check if this was the Client Characteristic Configuration Descriptor
            if (descriptor.uuid == CCC_DESCRIPTOR_UUID) {
                if (gattStatus == GattStatus.SUCCESS) {
                    if (value.contentEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) ||
                        value.contentEquals(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
                    ) {
                        notifyingCharacteristics.add(descriptor.characteristic)
                    } else if (value.contentEquals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
                    ) {
                        notifyingCharacteristics.remove(descriptor.characteristic)
                    }
                }
            }
            bleHandler.post {
                blePeripheralCallback.onDescriptorRead(
                    this@BlePeripheral,
                    value,
                    descriptor,
                    gattStatus
                )
            }
            completedCommand()
        }

        //@RequiresApi(Build.VERSION_CODES.TIRAMISU)
        @Deprecated("Deprecated in Java")
        override fun onDescriptorRead(
            gatt: BluetoothGatt,
            descriptor: BluetoothGattDescriptor,
            status: Int
        ) {
            if (Build.VERSION.SDK_INT < 33) {
                onDescriptorRead(gatt, descriptor, status, nonnullOf(descriptor.value))
            }
        }

        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray
        ) {
            Timber.tag("BlePeripheral").e("onCharacteristicChanged: ${value.bytesToHex()}")
            bleHandler.post {
                if (characteristic == rxChar) {
                    blePeripheralCallback.onReceiveResponse(value)
                } else if (characteristic == rxJLChar) {
                    blePeripheralCallback.onReceiveJLResponse(value)
                }
            }
        }


        @Deprecated(
            "Deprecated in Java",
            replaceWith = ReplaceWith("onCharacteristicChanged(gatt, characteristic, value)")
        )
        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic
        ) {
            if (Build.VERSION.SDK_INT < 33) {
                onCharacteristicChanged(gatt, characteristic, nonnullOf(characteristic.value))
            }
        }

        override fun onCharacteristicRead(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray,
            status: Int
        ) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(
                    TAG,
                    "read failed for characteristic <%s>, status '%s'",
                    characteristic.uuid,
                    gattStatus
                )
            }

            bleHandler.post {
                blePeripheralCallback.onCharacteristicUpdate(
                    this@BlePeripheral,
                    value,
                    characteristic,
                    gattStatus
                )
            }
            completedCommand()
        }

        @Deprecated("Deprecated in Java")
        override fun onCharacteristicRead(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            status: Int
        ) {
            if (Build.VERSION.SDK_INT < 33) {
                onCharacteristicRead(gatt, characteristic, nonnullOf(characteristic.value), status)
            }
        }

        override fun onCharacteristicWrite(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            status: Int
        ) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if ((characteristic == txChar || characteristic == txJLChar) && gattStatus == GattStatus.SUCCESS) {
                completedCommand()
            } else {
                Logger.e(
                    TAG,
                    "writing <%s> to characteristic <%s> failed, status '%s'",
                    "",
                    characteristic.uuid,
                    gattStatus
                )
            }
        }

        override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(TAG, "reading RSSI failed, status '%s'", gattStatus)
            }

            bleHandler.post {
                blePeripheralCallback.onReadRemoteRssi(
                    this@BlePeripheral,
                    rssi,
                    gattStatus
                )
            }
            completedCommand()
        }

        override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(TAG, "change MTU failed, status '%s'", gattStatus)
            }

            currentMtu = if (mtu > 512) 512 else mtu
            bleHandler.post {
                blePeripheralCallback.onMtuChanged(
                    this@BlePeripheral,
                    mtu,
                    gattStatus
                )
            }

            // Only complete the command if we initiated the operation. It can also be initiated by the remote peripheral...
            if (currentCommand == REQUEST_MTU_COMMAND) {
                currentCommand = IDLE
                completedCommand()
            }
        }

        override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(TAG, "read Phy failed, status '%s'", gattStatus)
            } else {
                Logger.i(
                    TAG,
                    "updated Phy: tx = %s, rx = %s",
                    PhyType.Companion.fromValue(txPhy),
                    PhyType.Companion.fromValue(rxPhy)
                )
            }
            bleHandler.post {
                blePeripheralCallback.onPhyUpdate(
                    this@BlePeripheral,
                    PhyType.Companion.fromValue(txPhy),
                    PhyType.Companion.fromValue(rxPhy),
                    gattStatus
                )
            }
            completedCommand()
        }

        override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus != GattStatus.SUCCESS) {
                Logger.e(TAG, "update Phy failed, status '%s'", gattStatus)
            } else {
                Logger.i(
                    TAG,
                    "updated Phy: tx = %s, rx = %s",
                    PhyType.Companion.fromValue(txPhy),
                    PhyType.Companion.fromValue(rxPhy)
                )
            }
            bleHandler.post {
                blePeripheralCallback.onPhyUpdate(
                    this@BlePeripheral,
                    PhyType.Companion.fromValue(txPhy),
                    PhyType.Companion.fromValue(rxPhy),
                    gattStatus
                )
            }

            // Only complete the command if we initiated the operation. It can also be initiated by the remote peripheral...
            if (currentCommand == SET_PHY_TYPE_COMMAND) {
                currentCommand = IDLE
                completedCommand()
            }
        }

        /**
         * This callback is only called from Android 8 (Oreo) or higher. Not all phones seem to call this though...
         */
        fun onConnectionUpdated(
            gatt: BluetoothGatt,
            interval: Int,
            latency: Int,
            timeout: Int,
            status: Int
        ) {
            if (gatt != bluetoothGatt) return
            val gattStatus = GattStatus.Companion.fromValue(status)
            if (gattStatus == GattStatus.SUCCESS) {
                val msg = String.format(
                    Locale.ENGLISH,
                    "connection parameters: interval=%.1fms latency=%d timeout=%ds",
                    interval * 1.25f,
                    latency,
                    timeout / 100
                )
                Logger.d(TAG, msg)
            } else {
                Logger.e(TAG, "connection parameters update failed with status '%s'", gattStatus)
            }
            bleHandler.post {
                blePeripheralCallback.onConnectionUpdated(
                    this@BlePeripheral,
                    interval,
                    latency,
                    timeout,
                    gattStatus
                )
            }
        }

        override fun onServiceChanged(gatt: BluetoothGatt) {
            Logger.d(TAG, "onServiceChangedCalled")

            // Does it really make sense to discover services? Or should we just disconnect and reconnect?
            commandQueue.clear()
            commandQueueBusy = false
            clearServicesCache()
            delayedDiscoverServices(200)
        }
    }

    internal fun successfullyConnected() {
        val timePassed = SystemClock.elapsedRealtime() - connectTimestamp
        Logger.i(TAG, "connected to '%s' (%s) in %.1fs", name, bondState, timePassed / 1000.0f)

        if (bondState == BondState.NONE || bondState == BondState.BONDED) {
            delayedDiscoverServices(200)
        } else if (bondState == BondState.BONDING) {
            // Apparently the bonding process has already started, so let it complete. We'll do discoverServices once bonding finished
            Logger.i(TAG, "waiting for bonding to complete")
        }
    }

    internal fun delayedDiscoverServices(delay: Long) {
        discoverServicesRunnable = Runnable {
            Logger.d(TAG, "discovering services of '%s' with delay of %d ms", name, delay)
            if (bluetoothGatt?.discoverServices() == true) {
                discoveryStarted = true
            } else {
                Logger.e(TAG, "discoverServices failed to start")
            }
            discoverServicesRunnable = null
        }

        discoverServicesRunnable?.let {
            bleHandler.postDelayed(it, delay)
        }
    }

    internal fun successfullyDisconnected(previousState: Int) {
        if (previousState == BluetoothProfile.STATE_CONNECTED || previousState == BluetoothProfile.STATE_DISCONNECTING) {
            Logger.i(TAG, "disconnected '%s' on request", name)
        } else if (previousState == BluetoothProfile.STATE_CONNECTING) {
            Logger.i(TAG, "cancelling connect attempt")
        }
        completeDisconnect(true, HciStatus.SUCCESS)
    }

    internal fun connectionStateChangeUnsuccessful(
        status: HciStatus,
        previousState: Int,
        newState: Int
    ) {
        cancelPendingServiceDiscovery()
        val servicesDiscovered = services.isNotEmpty()

        // See if the initial connection failed
        if (previousState == BluetoothProfile.STATE_CONNECTING) {
            if (status == HciStatus.ERROR) {
                bluetoothGatt?.let { GattUtil.clearGattCache(it) }
            }
            val timePassed = SystemClock.elapsedRealtime() - connectTimestamp
            val isTimeout = timePassed > timeoutThreshold
            val adjustedStatus =
                if (status == HciStatus.ERROR && isTimeout) HciStatus.CONNECTION_FAILED_ESTABLISHMENT else status
            Logger.i(TAG, "connection failed with status '%s'", adjustedStatus)
            completeDisconnect(false, adjustedStatus)
            blePeripheralCallback.connectFailed(this@BlePeripheral, adjustedStatus)
        } else if (previousState == BluetoothProfile.STATE_CONNECTED && newState == BluetoothProfile.STATE_DISCONNECTED && !servicesDiscovered) {
            // We got a disconnection before the services were even discovered
            Logger.i(
                TAG,
                "peripheral '%s' disconnected with status '%s' (%d) before completing service discovery",
                name,
                status,
                status.value
            )
            completeDisconnect(false, status)
            blePeripheralCallback.connectFailed(this@BlePeripheral, status)
        } else {
            // See if we got connection drop
            if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Logger.i(
                    TAG,
                    "peripheral '%s' disconnected with status '%s' (%d)",
                    name,
                    status,
                    status.value
                )
            } else {
                Logger.i(
                    TAG,
                    "unexpected connection state change for '%s' status '%s' (%d)",
                    name,
                    status,
                    status.value
                )
            }
            completeDisconnect(true, status)
        }
    }

    private fun cancelPendingServiceDiscovery() {
        discoverServicesRunnable?.let { bleHandler.removeCallbacks(it) }
        discoverServicesRunnable = null
    }

    /**
     * Connect directly with the bluetooth device. This call will timeout in max 30 seconds (5 seconds on Samsung phones)
     */
    private fun connect(): Boolean {
        Logger.i(TAG, "attempting to connect to '%s' (%s)", name, address)
        blePeripheralCallback.launchBleTimeoutTimer()
        // Make sure we are disconnected before we start making a connection
        if (state == BluetoothProfile.STATE_DISCONNECTED) {
            state = BluetoothProfile.STATE_CONNECTING
            bleHandler.postDelayed({
                Logger.i(TAG, "connect to '%s' (%s)", name, address)
                discoveryStarted = false
                connectTimestamp = SystemClock.elapsedRealtime()
                bluetoothGatt = null
                bluetoothGatt = try {
                    bleDevice.connectGatt(
                        context,
                        false,
                        bluetoothGattCallback,
                        BluetoothDevice.TRANSPORT_LE
                    )
                } catch (e: SecurityException) {
                    Logger.d(TAG, "exception when calling connectGatt")
                    null
                }

                bluetoothGatt?.let {
                    bluetoothGattCallback.onConnectionStateChange(
                        bluetoothGatt,
                        HciStatus.SUCCESS.value,
                        BluetoothProfile.STATE_CONNECTING
                    )
                    startConnectionTimer(this@BlePeripheral)
                }
            }, DIRECT_CONNECTION_DELAY_IN_MS)
            return true
        }
        Logger.e(TAG, "peripheral '%s' not (yet) disconnected, will not connect", name)
        return false
    }

    /**
     * Cancel an active or pending connection.
     *
     *
     * This operation is asynchronous and you will receive a callback on onDisconnectedPeripheral.
     */
    fun cancelConnection() {
        if (bluetoothGatt == null) {
            Logger.w(TAG, "cannot cancel connection because no connection attempt is made yet")
            return
        }

        if (state == BluetoothProfile.STATE_DISCONNECTED || state == BluetoothProfile.STATE_DISCONNECTING) {
            return
        }

        cancelConnectionTimer()

        // Check if we are in the process of connecting
        if (state == BluetoothProfile.STATE_CONNECTING) {
            // Cancel the connection by calling disconnect
            disconnect()

            // Since we will not get a callback on onConnectionStateChange for this, we issue the disconnect ourselves
            bleHandler.postDelayed({
                if (bluetoothGatt != null) {
                    bluetoothGattCallback.onConnectionStateChange(
                        bluetoothGatt,
                        HciStatus.SUCCESS.value,
                        BluetoothProfile.STATE_DISCONNECTED
                    )
                }
            }, 50)
        } else {
            // Cancel active connection and onConnectionStateChange will be called by Android
            disconnect()
        }
    }

    /**
     * Disconnect the bluetooth peripheral.
     *
     */
    fun disconnect() {
        if (state == BluetoothProfile.STATE_CONNECTED || state == BluetoothProfile.STATE_CONNECTING) {
            if (bluetoothGatt != null) {
                bluetoothGattCallback.onConnectionStateChange(
                    bluetoothGatt,
                    HciStatus.SUCCESS.value,
                    BluetoothProfile.STATE_DISCONNECTING
                )
            }
            bleHandler.post {
                if (state == BluetoothProfile.STATE_DISCONNECTING) {
                    bluetoothGatt?.disconnect()
                    Logger.i(TAG, "force disconnect '%s' (%s)", name, address)
                }
            }
        } else {
            blePeripheralCallback.disconnected(this@BlePeripheral, HciStatus.SUCCESS)
        }
    }

    fun cancelBondProcess() {
        unregisterBondingBroadcastReceivers()
    }

    fun createBond(
        needBond: Boolean
    ): Boolean {
        Logger.i(TAG, "call createBond")

        if (!needBond) {
            return connect()
        }

        if (bleDevice.type == BluetoothDevice.DEVICE_TYPE_DUAL) {
            if (bleDevice.bondState == BOND_BONDED) {
                return connect()
            } else {
                registerBondingBroadcastReceivers()
                val ret = bleDevice.createBond()
                if (!ret) {
                    unregisterBondingBroadcastReceivers()
                    return connect()
                }
                return true
            }
        } else {
            return connect()
        }
    }

//    private fun createInternalBond(type: Int): Boolean {
//        try {
//            val method = bleDevice.javaClass.getMethod("createBond")
//            method.isAccessible = true
//            val ret = method.invoke(bleDevice, type)
//            return ret as Boolean
//        } catch (e: Exception) {
//            e.printStackTrace()
//        }
//        return false
//    }

    private var boardingReceiverRegistered = false

    private fun registerBondingBroadcastReceivers() {
        if (!boardingReceiverRegistered) {
            context.registerReceiver(
                bondStateReceiver,
                IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
            )
        }
    }

    private fun unregisterBondingBroadcastReceivers() {
        if (boardingReceiverRegistered) {
            try {
                context.unregisterReceiver(bondStateReceiver)
            } catch (e: IllegalArgumentException) {
                Timber.e("Receiver not registered: ${e.message}")
            }
            boardingReceiverRegistered = false
        }
    }

    private val bondStateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent) {
            val action = intent.action ?: return
            val receivedDevice =
                intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
                    ?: return

            // Ignore updates for other devices
            Timber.d("bondStateReceiver: $action, receivedDevice: $receivedDevice")
            if (!receivedDevice.address.equals(bleDevice.address, ignoreCase = true)) return

            if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
                val bondState =
                    intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
                val previousBondState = intent.getIntExtra(
                    BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE,
                    BluetoothDevice.ERROR
                )
                handleBondStateChange(bondState, previousBondState)
            }
        }
    }

    private fun handleBondStateChange(bondState: Int, previousBondState: Int) {
        Timber.d("bondState: $bondState, previousBondState: $previousBondState")
        when (bondState) {
            BOND_BONDING -> {}
            BOND_BONDED -> {
                goConnectBle()
            }

            BOND_NONE -> {
                if (previousBondState == BOND_BONDING || previousBondState == BOND_BONDED) {
                    goConnectBle()
                }
            }
        }
    }

    private fun goConnectBle() {
        Logger.i(TAG, "bonding process finished, connecting...$state")
        unregisterBondingBroadcastReceivers()
        if (state == BluetoothProfile.STATE_CONNECTED || state == BluetoothProfile.STATE_CONNECTING) {
            return
        }
        connect()
    }


    fun disconnectWhenBluetoothOff() {
        completeDisconnect(true, HciStatus.SUCCESS)
    }

    /**
     * Complete the disconnect after getting connectionstate == disconnected
     */
    private fun completeDisconnect(notify: Boolean, status: HciStatus) {
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
        bluetoothGatt = null
        commandQueue.clear()
        commandQueueBusy = false
        notifyingCharacteristics.clear()
        currentMtu = DEFAULT_MTU
        currentCommand = IDLE
        discoveryStarted = false
        if (notify) {
            blePeripheralCallback.disconnected(this@BlePeripheral, status)
        }
    }

    /**
     * Get the mac address of the bluetooth peripheral.
     *
     * @return Address of the bluetooth peripheral
     */
    val address: String
        get() = bleDevice.address


    /**
     * Get the name of the bluetooth peripheral.
     *
     * @return name of the bluetooth peripheral
     */
    val name: String
        get() {
            val deviceName = try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    if (ActivityCompat.checkSelfPermission(
                            context,
                            Manifest.permission.BLUETOOTH_CONNECT
                        ) == PackageManager.PERMISSION_GRANTED
                    ) {
                        bleDevice.name
                    } else {
                        null
                    }
                } else {
                    bleDevice.name
                }
            } catch (e: SecurityException) {
                null
            }

            if (deviceName != null) {
                // Cache the name so that we even know it when bluetooth is switched off
                cachedName = deviceName
                return deviceName
            }
            return cachedName
        }

    /**
     * Get the bond state of the bluetooth peripheral.
     *
     * @return the bond state
     */
    val bondState: BondState
        get() = BondState.Companion.fromValue(bleDevice.bondState)

    /**
     * Get the services supported by the connected bluetooth peripheral.
     *
     * @return Supported services.
     */
    val services: List<BluetoothGattService>
        get() = bluetoothGatt?.services ?: emptyList()

    /**
     * Get the BluetoothGattService object for a service UUID.
     *
     * @param serviceUUID the UUID of the service
     * @return the BluetoothGattService object for the service UUID or null if the peripheral does not have a service with the specified UUID
     */
    private fun getService(serviceUUID: UUID): BluetoothGattService? {
        return bluetoothGatt?.getService(serviceUUID)
    }

    /**
     * Get the BluetoothGattCharacteristic object for a characteristic UUID.
     *
     * @param serviceUUID        the service UUID the characteristic is part of
     * @param characteristicUUID the UUID of the characteristic
     * @return the BluetoothGattCharacteristic object for the characteristic UUID or null if the peripheral does not have a characteristic with the specified UUID
     */
    private fun getCharacteristic(
        serviceUUID: UUID,
        characteristicUUID: UUID
    ): BluetoothGattCharacteristic? {
        return getService(serviceUUID)?.getCharacteristic(characteristicUUID)
    }


    /**
     * Get maximum length of byte array that can be written depending on WriteType
     *
     * This value is derived from the current negotiated MTU or the maximum characteristic length (512)
     */
    private fun getMaximumWriteValueLength(): Int = (currentMtu - 3).coerceAtMost(512)

    /**
     * Boolean to indicate if the specified characteristic is currently notifying or indicating.
     *
     * @param characteristic the characteristic to check
     * @return true if the characteristic is notifying or indicating, false if it is not
     */
    fun isNotifying(characteristic: BluetoothGattCharacteristic): Boolean {
        return notifyingCharacteristics.contains(characteristic)
    }

    /**
     * Get all notifying/indicating characteristics
     *
     * @return Set of characteristics or empty set
     */
    fun getNotifyingCharacteristics(): Set<BluetoothGattCharacteristic> {
        return Collections.unmodifiableSet(notifyingCharacteristics)
    }

    private val isConnected: Boolean
        get() = bluetoothGatt != null && state == BluetoothProfile.STATE_CONNECTED

    private fun notConnected(): Boolean {
        return !isConnected
    }


    /**
     * Read the value of a characteristic.
     *
     * Convenience function to read a characteristic without first having to find it.
     *
     * @param serviceUUID        the service UUID the characteristic belongs to
     * @param characteristicUUID the characteristic's UUID
     * @return true if the characteristic was found and the operation was enqueued, otherwise false
     * @throws IllegalArgumentException if the characteristic does not support reading
     */
    fun readCharacteristic(serviceUUID: UUID, characteristicUUID: UUID): Boolean {
        val characteristic = getCharacteristic(serviceUUID, characteristicUUID)
        return characteristic?.let { readCharacteristic(it) } ?: false
    }

    /**
     * Read the value of a characteristic.
     *
     * @param characteristic Specifies the characteristic to read.
     * @return true if the operation was enqueued, otherwise false
     * @throws IllegalArgumentException if the characteristic does not support reading
     */
    private fun readCharacteristic(characteristic: BluetoothGattCharacteristic): Boolean {
        if (doesNotSupportReading(characteristic)) {
            val message = "characteristic <${characteristic.uuid}> does not have read property"
            throw IllegalArgumentException(message)
        }

        return enqueue {
            if (bluetoothGatt?.readCharacteristic(characteristic) == true) {
                Logger.d(TAG, "reading characteristic <%s>", characteristic.uuid)
                retries++
            } else {
                Logger.e(
                    TAG,
                    "readCharacteristic failed for characteristic: %s",
                    characteristic.uuid
                )
                completedCommand()
            }
        }
    }

    private fun doesNotSupportReading(characteristic: BluetoothGattCharacteristic): Boolean {
        return characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ == 0
    }

    fun writeJsonRequest(data: ByteArray): Boolean {
        if (txChar == null) {
            Logger.e(TAG, "txChar is null")
            return false
        }
        return writeCharacteristic(txChar!!, data)
    }

    fun writeJLRequest(data: ByteArray): Boolean {
        if (txJLChar == null) {
            Logger.e(TAG, "txJLChar is null")
            return false
        }
        return writeCharacteristic(txJLChar!!, data)
    }


    private fun BluetoothGattCharacteristic.supportsWriteType(writeType: WriteType): Boolean {
        val writeProperty: Int = when (writeType) {
            WriteType.WITH_RESPONSE -> BluetoothGattCharacteristic.PROPERTY_WRITE
            WriteType.WITHOUT_RESPONSE -> BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
            else -> 0
        }
        return properties and writeProperty > 0
    }

    private fun BluetoothGattCharacteristic.supportsIndicate(): Boolean {
        return properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0
    }

    private fun BluetoothGattCharacteristic.supportsNotify(): Boolean {
        return properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
    }


    /**
     * Write a value to a characteristic using the specified write type.
     *
     * All parameters must have a valid value in order for the operation to be enqueued.
     * The length of the byte array to write must be between 1 and getMaximumWriteValueLength(writeType).
     *
     * @param characteristic the characteristic to write to
     * @param value          the byte array to write
     * @return true if a write operation was successfully enqueued, otherwise false
     * @throws IllegalArgumentException if the characteristic does not support writing with the specified writeType or the byte array is empty or too long
     */
    private fun writeCharacteristic(
        characteristic: BluetoothGattCharacteristic,
        value: ByteArray
    ): Boolean {
        require(value.isNotEmpty()) { VALUE_STRING_IS_EMPTY }
//        if (!characteristic.supportsWriteType(WriteType.WITH_RESPONSE)) {
//            val message = "characteristic <${characteristic.uuid} does not support"
//            throw IllegalArgumentException(message)
//        }

        if (willCauseLongWrite(value)) {
            val sliceMTU = currentMtu - 3
            var packetIndex = 0
            while (packetIndex < value.size) {
                val end =
                    if (packetIndex + sliceMTU > value.size) value.size else packetIndex + sliceMTU
                val chunk = value.copyOfRange(packetIndex, end)
                packetIndex = end
                if (!enqueue { internalWriteCharacteristic(characteristic, chunk) }) {
                    completedCommand()
                    return false
                }
            }
            return true

        } else {
            return enqueue {
                if (internalWriteCharacteristic(characteristic, value)) {
                    Logger.w(
                        TAG,
                        "writing <%s> to characteristic <%s>",
                        value.bytesToHex(),
                        characteristic.uuid
                    )
                    retries++
                } else {
                    completedCommand()
                }
            }
        }
    }

    private fun willCauseLongWrite(value: ByteArray): Boolean {
        return value.size > currentMtu - 3
    }

    private fun internalWriteCharacteristic(
        characteristic: BluetoothGattCharacteristic,
        value: ByteArray
    ): Boolean {
        if (bluetoothGatt == null) return false
        val writeType = if (characteristic.supportsWriteType(WriteType.WITH_RESPONSE)) {
            WriteType.WITH_RESPONSE
        } else {
            WriteType.WITHOUT_RESPONSE
        }
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val result = bluetoothGatt?.writeCharacteristic(
                characteristic,
                value,
                writeType.writeType
            )
            result == BluetoothStatusCodes.SUCCESS
        } else {
            characteristic.writeType = writeType.writeType
            characteristic.setValue(value)
            bluetoothGatt?.writeCharacteristic(characteristic) ?: false
        }
    }

    /**
     * Read the value of a descriptor.
     *
     * Convenience function to read a descriptor without first having to find it.
     *
     * @param serviceUUID        the service UUID the characteristic belongs to
     * @param characteristicUUID the characteristic's UUID
     * @param descriptorUUID    the descriptor's UUID
     * @return true if the descriptor was found and the operation was enqueued, otherwise false
     * @throws IllegalArgumentException if the descriptor does not support reading
     */
    fun readDescriptor(serviceUUID: UUID, characteristicUUID: UUID, descriptorUUID: UUID): Boolean {
        val descriptor =
            getCharacteristic(serviceUUID, characteristicUUID)?.getDescriptor(descriptorUUID)
        return descriptor?.let { readDescriptor(it) } ?: false
    }

    /**
     * Read the value of a descriptor.
     *
     * @param descriptor the descriptor to read
     * @return true if a read operation was successfully enqueued, otherwise false
     */
    private fun readDescriptor(descriptor: BluetoothGattDescriptor): Boolean {
        return enqueue {
            if (bluetoothGatt?.readDescriptor(descriptor) == true) {
                Logger.d(TAG, "reading descriptor <%s>", descriptor.uuid)
                retries++
            } else {
                Logger.e(TAG, "readDescriptor failed for characteristic: %s", descriptor.uuid)
                completedCommand()
            }
        }
    }

    /**
     * Write the value of a descriptor.
     *
     * Convenience function to write a descriptor without first having to find it.
     *
     * @param serviceUUID        the service UUID the characteristic belongs to
     * @param characteristicUUID the characteristic's UUID
     * @param descriptorUUID    the descriptor's UUID
     * @param value      the value to write
     * @return true if the descriptor was found and the operation was enqueued, otherwise false
     * @throws IllegalArgumentException if the value is not valid
     */
    fun writeDescriptor(
        serviceUUID: UUID,
        characteristicUUID: UUID,
        descriptorUUID: UUID,
        value: ByteArray
    ): Boolean {
        val descriptor =
            getCharacteristic(serviceUUID, characteristicUUID)?.getDescriptor(descriptorUUID)
        return descriptor?.let { writeDescriptor(it, value) } ?: false
    }

    /**
     * Write a value to a descriptor.
     *
     * For turning on/off notifications use [BlePeripheral.startNotify] instead.
     *
     * @param descriptor the descriptor to write to
     * @param value      the value to write
     * @return true if a write operation was successfully enqueued, otherwise false
     * @throws IllegalArgumentException if the value is not valid
     */
    private fun writeDescriptor(descriptor: BluetoothGattDescriptor, value: ByteArray): Boolean {
        require(value.isNotEmpty()) { VALUE_STRING_IS_EMPTY }
        require(value.size <= getMaximumWriteValueLength()) { VALUE_STRING_IS_TOO_LONG }

        // Copy the value to avoid race conditions
        val bytesToWrite = copyOf(value)
        return enqueue {
            if (internalWriteDescriptor(descriptor, bytesToWrite)) {
                Logger.d(
                    TAG,
                    "writing <%s> to descriptor <%s>",
                    bytesToWrite.toString(),
                    descriptor.uuid
                )
                retries++
            } else {
                Logger.e(TAG, "writeDescriptor failed for descriptor: %s", descriptor.uuid)
                completedCommand()
            }
        }
    }

    private fun internalWriteDescriptor(
        descriptor: BluetoothGattDescriptor,
        value: ByteArray
    ): Boolean {
        if (bluetoothGatt == null) return false
        return if (Build.VERSION.SDK_INT >= 33) {
            val result = bluetoothGatt?.writeDescriptor(descriptor, value)
            result == BluetoothStatusCodes.SUCCESS
        } else {
            descriptor.value = value
            bluetoothGatt?.writeDescriptor(descriptor) ?: false
        }
    }

    private fun startNotify(serviceUUID: UUID, characteristicUUID: UUID): Boolean {
        return setNotify(serviceUUID, characteristicUUID, true)
    }

    private fun startNotify(characteristic: BluetoothGattCharacteristic): Boolean {
        return setNotify(characteristic, true)
    }

    fun stopNotify(serviceUUID: UUID, characteristicUUID: UUID): Boolean {
        return setNotify(serviceUUID, characteristicUUID, false)
    }

    fun stopNotify(characteristic: BluetoothGattCharacteristic): Boolean {
        return setNotify(characteristic, false)
    }

    /**
     * Set the notification state of a characteristic to 'on' or 'off'. The characteristic must support notifications or indications.
     *
     * @param serviceUUID        the service UUID the characteristic belongs to
     * @param characteristicUUID the characteristic's UUID
     * @param enable             true for setting notification on, false for turning it off
     * @return true if the operation was enqueued, otherwise false
     * @throws IllegalArgumentException if the CCC descriptor was not found or the characteristic does not support notifications or indications
     */
    private fun setNotify(serviceUUID: UUID, characteristicUUID: UUID, enable: Boolean): Boolean {
        val characteristic = getCharacteristic(serviceUUID, characteristicUUID)
        return characteristic?.let { setNotify(it, enable) } ?: false
    }

    /**
     * Set the notification state of a characteristic to 'on' or 'off'. The characteristic must support notifications or indications.
     *
     *
     * @param characteristic the characteristic to turn notification on/off for
     * @param enable         true for setting notification on, false for turning it off
     * @return true if the operation was enqueued, otherwise false
     * @throws IllegalArgumentException if the CCC descriptor was not found or the characteristic does not support notifications or indications
     */
    internal fun setNotify(characteristic: BluetoothGattCharacteristic, enable: Boolean): Boolean {
        // Get the Client Characteristic Configuration Descriptor for the characteristic
        val descriptor = characteristic.getDescriptor(CCC_DESCRIPTOR_UUID)
        if (descriptor == null) {
            val message = String.format(
                "could not get CCC descriptor for characteristic %s",
                characteristic.uuid
            )
            throw IllegalArgumentException(message)
        }

        // Check if characteristic has NOTIFY or INDICATE properties and set the correct byte value to be written
        val value = when {
            characteristic.supportsNotify() -> {
                BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            }

            characteristic.supportsIndicate() -> {
                BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
            }

            else -> {
                val message = String.format(
                    "characteristic %s does not have notify or indicate property",
                    characteristic.uuid
                )
                throw IllegalArgumentException(message)
            }
        }
        val finalValue = if (enable) value else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE

        return enqueue {
            // First try to set notification for Gatt object
            if (bluetoothGatt?.setCharacteristicNotification(characteristic, enable) == false) {
                Logger.e(
                    TAG,
                    "setCharacteristicNotification failed for characteristic: %s",
                    characteristic.uuid
                )
                completedCommand()
                return@enqueue
            }

            // Then write to CCC descriptor
            if (internalWriteDescriptor(descriptor, finalValue)) {
                retries++
            } else {
                Logger.e(TAG, "writeDescriptor failed for descriptor: %s", descriptor.uuid)
                completedCommand()
            }
        }
    }

    /**
     * Read the RSSI for a connected remote peripheral.
     *
     * @return true if the operation was enqueued, false otherwise
     */
    fun readRemoteRssi(): Boolean {
        return enqueue {
            if (bluetoothGatt?.readRemoteRssi() == false) {
                Logger.e(TAG, "readRemoteRssi failed")
                completedCommand()
            }
        }
    }

    /**
     * Request an MTU size used for a given connection.
     *
     * When performing a write request operation (write without response),
     * the data sent is truncated to the MTU size. This function may be used
     * to request a larger MTU size to be able to send more data at once.
     *
     * Note that requesting an MTU should only take place once per connection, according to the Bluetooth standard.
     *
     * @param mtu the desired MTU size (must be between 23 and 517)
     * @return true if the operation was enqueued, false otherwise
     */
    internal fun requestMtu(mtu: Int): Boolean {
        require(!(mtu < DEFAULT_MTU || mtu > MAX_MTU)) { "mtu must be between $DEFAULT_MTU and $MAX_MTU" }

        return enqueue {
            if (bluetoothGatt?.requestMtu(mtu) == true) {
                currentCommand = REQUEST_MTU_COMMAND
                Logger.i(TAG, "requesting MTU of %d", mtu)
            } else {
                Logger.e(TAG, "requestMtu failed")
                completedCommand()
            }
        }
    }

    /**
     * Request a different connection priority.
     *
     * @param priority the requested connection priority
     * @return true if request was enqueued, false if not
     */
    fun requestConnectionPriority(priority: ConnectionPriority): Boolean {
        return enqueue {
            if (bluetoothGatt?.requestConnectionPriority(priority.value) == true) {
                Logger.d(TAG, "requesting connection priority %s", priority)
            } else {
                Logger.e(TAG, "could not request connection priority")
            }

            // Complete command as there is no reliable callback for this, but allow some time
            bleHandler.postDelayed({ completedCommand() }, AVG_REQUEST_CONNECTION_PRIORITY_DURATION)
        }
    }

    /**
     * Set the preferred connection PHY for this app. Please note that this is just a
     * recommendation, whether the PHY change will happen depends on other applications preferences,
     * local and remote controller capabilities. Controller can override these settings.
     *
     * if no PHY change happens. It is also triggered when remote device updates the PHY.
     *
     * @param txPhy      the desired TX PHY
     * @param rxPhy      the desired RX PHY
     * @param phyOptions the desired optional sub-type for PHY_LE_CODED
     * @return true if request was enqueued, false if not
     */
    @RequiresApi(Build.VERSION_CODES.O)
    fun setPreferredPhy(txPhy: PhyType, rxPhy: PhyType, phyOptions: PhyOptions): Boolean {
        return enqueue {
            currentCommand = SET_PHY_TYPE_COMMAND
            Logger.i(
                TAG,
                "setting preferred Phy: tx = %s, rx = %s, options = %s",
                txPhy,
                rxPhy,
                phyOptions
            )
            bluetoothGatt?.setPreferredPhy(txPhy.mask, rxPhy.mask, phyOptions.value)

            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
                // There is a bug in Android 13 where onPhyUpdate is not always called
                // Therefore complete this command after a delay in order not to block the queue
                currentCommand = IDLE
                bleHandler.postDelayed({ completedCommand() }, 200)
            }
        }
    }

    /**
     * Read the current transmitter PHY and receiver PHY of the connection. The values are returned
     */
    fun readPhy(): Boolean {
        return enqueue {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                bluetoothGatt?.readPhy()
            }
            Logger.d(TAG, "reading Phy")
        }
    }

    /**
     * Asynchronous method to clear the services cache. Make sure to add a delay when using this!
     *
     * @return true if the method was executed, false if not executed
     */
    fun clearServicesCache(): Boolean {
        if (bluetoothGatt == null) return false
        var result = false
        try {
            val refreshMethod = bluetoothGatt?.javaClass?.getMethod("refresh")
            if (refreshMethod != null) {
                result = refreshMethod.invoke(bluetoothGatt) as Boolean
            }
        } catch (e: Exception) {
            Logger.e(TAG, "could not invoke refresh method")
        }
        return result
    }

    /**
     * Enqueue a runnable to the command queue
     *
     * @param command a Runnable containg a command
     * @return true if the command was successfully enqueued, otherwise false
     */
    private fun enqueue(command: Runnable): Boolean {
        if (notConnected()) {
            Logger.e(TAG, PERIPHERAL_NOT_CONNECTED)
            return false
        }

        val result = commandQueue.add(command)
        if (result) {
            nextCommand()
        } else {
            Logger.e(TAG, "could not enqueue command")
        }
        return result
    }

    /**
     * The current command has been completed, move to the next command in the queue (if any)
     */
    internal fun completedCommand() {
        isRetrying = false
        commandQueue.poll()
        commandQueueBusy = false
        nextCommand()
    }

    /**
     * Retry the current command. Typically used when a read/write fails and triggers a bonding procedure
     */
    private fun retryCommand() {
        commandQueueBusy = false
        val currentCommand = commandQueue.peek()
        if (currentCommand != null) {
            if (retries >= MAX_TRIES) {
                // Max retries reached, give up on this one and proceed
                Logger.d(TAG, "max number of tries reached, not retrying operation anymore")
                commandQueue.poll()
            } else {
                isRetrying = true
            }
        }
        nextCommand()
    }

    /**
     * Execute the next command in the command queue.
     * If a command is being executed the next command will not be executed
     * A queue is used because the calls have to be executed sequentially.
     * If the command fails, the next command in the queue is executed.
     */
    private fun nextCommand() {
        synchronized(this) {

            // If there is still a command being executed, then bail out
            if (commandQueueBusy) return

            // Check if there is something to do at all
            val bluetoothCommand = commandQueue.peek() ?: return

            // Check if we still have a valid gatt object
            if (bluetoothGatt == null) {
                Logger.e(TAG, "gatt is 'null' for peripheral '%s', clearing command queue", address)
                commandQueue.clear()
                commandQueueBusy = false
                return
            }

            // Check if the peripheral has initiated bonding as this may be a reason for failures
            if (bondState == BondState.BONDING) {
                Logger.w(TAG, "bonding is in progress, waiting for bonding to complete")
                return
            }

            // Execute the next command in the queue
            commandQueueBusy = true
            if (!isRetrying) {
                retries = 0
            }
            bleHandler.post {
                try {
                    if (isConnected) {
                        bluetoothCommand.run()
                    }
                } catch (ex: Exception) {
                    Logger.e(TAG, "command exception for device '%s'", name)
                    Logger.e(TAG, ex.toString())
                    completedCommand()
                }
            }
        }
    }

    private fun pairingVariantToString(variant: Int): String {
        return when (variant) {
            PAIRING_VARIANT_PIN -> "PAIRING_VARIANT_PIN"
            PAIRING_VARIANT_PASSKEY -> "PAIRING_VARIANT_PASSKEY"
            PAIRING_VARIANT_PASSKEY_CONFIRMATION -> "PAIRING_VARIANT_PASSKEY_CONFIRMATION"
            PAIRING_VARIANT_CONSENT -> "PAIRING_VARIANT_CONSENT"
            PAIRING_VARIANT_DISPLAY_PASSKEY -> "PAIRING_VARIANT_DISPLAY_PASSKEY"
            PAIRING_VARIANT_DISPLAY_PIN -> "PAIRING_VARIANT_DISPLAY_PIN"
            PAIRING_VARIANT_OOB_CONSENT -> "PAIRING_VARIANT_OOB_CONSENT"
            else -> "UNKNOWN"
        }
    }

    private fun startConnectionTimer(peripheral: BlePeripheral) {
        cancelConnectionTimer()
        timeoutRunnable = Runnable {
            Logger.e(TAG, "connection timeout, disconnecting '%s'", peripheral.name)
            bluetoothGatt?.let {
                bluetoothGattCallback.onConnectionStateChange(
                    it,
                    HciStatus.CONNECTION_FAILED_ESTABLISHMENT.value,
                    BluetoothProfile.STATE_DISCONNECTED
                )
            }
            timeoutRunnable = null
        }

        timeoutRunnable?.let {
            bleHandler.postDelayed(it, CONNECTION_TIMEOUT_IN_MS.toLong())
        }
    }

    internal fun cancelConnectionTimer() {
        timeoutRunnable?.let {
            bleHandler.removeCallbacks(it)
        }
        timeoutRunnable = null
    }

    private val timeoutThreshold: Int
        get() {
            val manufacturer = Build.MANUFACTURER
            return if (manufacturer.equals("samsung", ignoreCase = true)) {
                TIMEOUT_THRESHOLD_SAMSUNG
            } else {
                TIMEOUT_THRESHOLD_DEFAULT
            }
        }

    /**
     * Make a safe copy of a nullable byte array
     *
     * @param source byte array to copy
     * @return non-null copy of the source byte array or an empty array if source was null
     */
    private fun copyOf(source: ByteArray?): ByteArray {
        return if (source == null) ByteArray(0) else Arrays.copyOf(source, source.size)
    }

    /**
     * Make a byte array nonnull by either returning the original byte array if non-null or an empty bytearray
     *
     * @param source byte array to make nonnull
     * @return the source byte array or an empty array if source was null
     */
    fun nonnullOf(source: ByteArray?): ByteArray {
        return source ?: ByteArray(0)
    }

    companion object {
        private val TAG = BlePeripheral::class.java.simpleName

        // This is used for the notification enable
        private val CCC_DESCRIPTOR_UUID =
            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

        // This is used for the get Device main UUID
        private val MAIN_SERVICE_UUID =
            UUID.fromString("66880000-0000-1000-8000-008012563489")

        // This is used for the Call UART
        private val TX_CHAR_UUID = UUID.fromString("66880001-0000-1000-8000-008012563489")

        // This is used for the Read data
        private val RX_CHAR_UUID = UUID.fromString("66880002-0000-1000-8000-008012563489")

        //Jie Li SDK
        private val JL_SERVICE_UUID = UUID.fromString("0000AE00-0000-1000-8000-00805f9b34fb")
        private val JL_TX_UUID = UUID.fromString("0000AE01-0000-1000-8000-00805f9b34fb")
        private val JL_RX_UUID = UUID.fromString("0000AE02-0000-1000-8000-00805f9b34fb")


        /**
         * Max MTU that Android can handle
         */
        const val MAX_MTU = 517

        // Minimal and default MTU
        private const val DEFAULT_MTU = 23

        // Maximum number of retries of commands
        private const val MAX_TRIES = 2

        // Delay to use when doing a connect
        private const val DIRECT_CONNECTION_DELAY_IN_MS = 500L

        // Timeout to use if no callback on onConnectionStateChange happens
        private const val CONNECTION_TIMEOUT_IN_MS = 35000

        // Samsung phones time out after 5 seconds while most other phone time out after 30 seconds
        private const val TIMEOUT_THRESHOLD_SAMSUNG = 4500

        // Most other phone time out after 30 seconds
        private const val TIMEOUT_THRESHOLD_DEFAULT = 25000

        // When a bond is lost, the bluetooth stack needs some time to update its internal state
        private const val DELAY_AFTER_BOND_LOST = 1000L

        // The average time it takes to complete requestConnectionPriority
        private const val AVG_REQUEST_CONNECTION_PRIORITY_DURATION: Long = 500
        private const val NO_VALID_SERVICE_UUID_PROVIDED = "no valid service UUID provided"
        private const val NO_VALID_CHARACTERISTIC_UUID_PROVIDED =
            "no valid characteristic UUID provided"
        private const val NO_VALID_CHARACTERISTIC_PROVIDED = "no valid characteristic provided"
        private const val NO_VALID_WRITE_TYPE_PROVIDED = "no valid writeType provided"
        private const val NO_VALID_VALUE_PROVIDED = "no valid value provided"
        private const val NO_VALID_DESCRIPTOR_PROVIDED = "no valid descriptor provided"
        private const val NO_VALID_PERIPHERAL_CALLBACK_PROVIDED =
            "no valid peripheral callback provided"
        private const val NO_VALID_DEVICE_PROVIDED = "no valid device provided"
        private const val NO_VALID_PRIORITY_PROVIDED = "no valid priority provided"
        private const val PERIPHERAL_NOT_CONNECTED = "peripheral not connected"
        private const val VALUE_STRING_IS_EMPTY = "value string is empty"
        private const val VALUE_STRING_IS_TOO_LONG = "value string is too long"

        // String constants for commands where the callbacks can also happen because the remote peripheral initiated the command
        private const val IDLE = 0
        private const val REQUEST_MTU_COMMAND = 1
        private const val SET_PHY_TYPE_COMMAND = 2
        private const val PAIRING_VARIANT_PIN = 0
        private const val PAIRING_VARIANT_PASSKEY = 1
        private const val PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2
        private const val PAIRING_VARIANT_CONSENT = 3
        private const val PAIRING_VARIANT_DISPLAY_PASSKEY = 4
        private const val PAIRING_VARIANT_DISPLAY_PIN = 5
        private const val PAIRING_VARIANT_OOB_CONSENT = 6

        /** Address type random static bits value */
        private const val ADDRESS_TYPE_RANDOM_STATIC_BITS_VALUE: Int = 3

        /** Address type random resolvable bits value */
        private const val ADDRESS_TYPE_RANDOM_RESOLVABLE_BITS_VALUE: Int = 1

        /** Address type random non resolvable bits value */
        private const val ADDRESS_TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE: Int = 0
    }
}